﻿namespace Numbrella.ProblemProvider
{
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Globalization;
	using System.IO;
	using System.Reflection;
	using System.ServiceModel;
	using Numbrella.Client.Common;
	using Numbrella.Core;
	using Numbrella.Interaction;

	internal class ProblemProvidingEngine
	{
		#region Fields

		private readonly BackgroundWorker backgroundWorker;

		private ProblemBase problem;

		private ServiceHost problemServiceHost;

		private bool problemWasAdded;

		#endregion

		public ProblemProvidingEngine()
		{
			this.backgroundWorker = new BackgroundWorker { WorkerSupportsCancellation = true };
			this.backgroundWorker.DoWork += this.OnBackgroundWorkerDoWork;
			this.backgroundWorker.RunWorkerCompleted += this.OnBackgroundWorkerRunWorkerCompleted;
		}

		public event AsyncCompletedEventHandler SolveProblemCompleted;

		public event EventHandler ProblemInformationChanged;

		public ProblemBase CurrentProblem
		{
			get
			{
				return this.problem;
			}

			private set
			{
				this.problem = value;
				this.RaiseProblemInformationChanged();
			}
		}

		#region Async pattern

		public void SolveProblemAsync(string problemAssemblyPath)
		{
			Debug.Assert(!String.IsNullOrEmpty(problemAssemblyPath), "File name can not be null or empty.");
			Debug.Assert(this.CurrentProblem == null, "Problem providing should not be started yet.");

			this.backgroundWorker.RunWorkerAsync(problemAssemblyPath);
		}

		public void CancelAsync()
		{
			Debug.Assert(this.CurrentProblem != null, "Problem providing should be started.");

			Logger.Instance.AddRecord("Cancelling problem providing...");
			this.backgroundWorker.CancelAsync();
		}

		#endregion

		#region Background worker stuff

		private void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
		{
			this.problemWasAdded = false;
			this.LoadProblemFromFile((string) e.Argument);
			this.CreateServiceHostForProblem();

			Logger.Instance.AddRecord(String.Format("Solving of your problem '{0}' was started.", this.CurrentProblem.Name));

			while (!this.backgroundWorker.CancellationPending && !this.CurrentProblem.IsCompleted)
			{
				if (!this.CurrentProblem.GetIfHasTasksToSolve())
				{
					CommonFunctions.Timeout(Settings.Current.TrackerNotificationTimeout);
					continue;
				}

				TrackerProxy trackerProxy = CommonFunctions.OpenConnectionToTracker(
					Settings.Current.TrackerConnectionString, Settings.Current.UserName, Settings.Current.Password);
				bool wasError = false;

				try
				{
					if (!this.problemWasAdded)
					{
						trackerProxy.AddProblem(this.CurrentProblem.Name, this.CurrentProblem.Description, this.CurrentProblem.Implementation);
						Logger.Instance.AddRecord(String.Format("Your problem '{0}' was successfully added to tracker.", this.problem.Name));
						this.problemWasAdded = true;
					}

					trackerProxy.ReadyForSeed(this.problem.Name, Settings.Current.ProblemHostingPort);
					trackerProxy.Close();
					CommonFunctions.Timeout(Settings.Current.TrackerNotificationTimeout);
				}
				catch (TimeoutException exception)
				{
					Logger.Instance.AddRecord("Network operation timed out. " + CommonFunctions.UnwrapExceptionMessage(exception));
					wasError = true;
				}
				catch (CommunicationException exception)
				{
					Logger.Instance.AddRecord("There was a communication problem with tracker. " + CommonFunctions.UnwrapExceptionMessage(exception));
					wasError = true;
				}

				if (wasError)
				{
					CommonFunctions.Timeout(Settings.Current.ErrorTimeout);
					trackerProxy.Abort();
				}
			}

			if (!this.CurrentProblem.IsCompleted)
				e.Cancel = true;

			if (this.problemWasAdded)
				this.DeleteProblemFromTracker();

			// Give clients time to know that problem is not available anymore
			CommonFunctions.Timeout(5);

			this.problemServiceHost.Close();
			this.CurrentProblem = null;
		}

		private void OnBackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			Logger.Instance.AddRecord("Problem providing {0}.", e.Cancelled ? "cancelled" : "stopped");

			this.RaiseSolveProblemCompleted(new AsyncCompletedEventArgs(e.Error, e.Cancelled, e.UserState));
		}

		#endregion

		private Type GetFactoryType(Assembly problemAssembly)
		{
			Type[] types = problemAssembly.GetExportedTypes();
			Type factoryType = null;
			foreach (Type type in types)
			{
				if (type.GetInterface(typeof(IProblemFactory).Name) != null)
				{
					factoryType = type;
					break;
				}
			}

			return factoryType;
		}

		private void CreateServiceHostForProblem()
		{
			Debug.Assert(this.CurrentProblem != null, "Problem should be initialized there.");

			// Specify known types for the serializer
			KnownTypeManager.FillTypes(this.problem.TypeOfTask, this.problem.TypeOfResult);

			// Create WCF service host
			Uri endpointAddress = new Uri(
				string.Format(CultureInfo.InvariantCulture, "net.tcp://localhost:{0}/{1}", Settings.Current.ProblemHostingPort, this.problem.Name));
			this.problemServiceHost = new ServiceHost(this.problem);
			this.problemServiceHost.AddServiceEndpoint(typeof(IProblem), new NetTcpBinding(SecurityMode.None), endpointAddress);
			this.problemServiceHost.Open();
		}

		private void LoadProblemFromFile(string fileName)
		{
			// Load assembly
			Assembly assembly = Assembly.LoadFile(Path.GetFullPath(fileName));

			// Create problem factory
			Type factoryType = this.GetFactoryType(assembly);
			if (factoryType == null)
				throw new ClientException(
					string.Format(
						CultureInfo.CurrentCulture,
						"No problem factory defined in assembly '{0}'.",
						Path.GetFileName(fileName)));
			IProblemFactory factory = (IProblemFactory) Activator.CreateInstance(factoryType);

			// Create problem
			this.CurrentProblem = factory.CreateProblem();
			this.CurrentProblem.TaskSolved += this.OnCurrentProblemTaskSolved;
		}

		private void OnCurrentProblemTaskSolved(object sender, TaskSolvedEventArgs e)
		{
			Logger.Instance.AddRecord(string.Format("Task {0} of your problem '{1}' was solved.", e.TaskId, this.CurrentProblem.Name));

			this.NotifyTrackerAboutSolvedTask(e.TaskId, e.UserNames);
			this.RaiseProblemInformationChanged();
		}

		#region Event invocators

		private void RaiseProblemInformationChanged()
		{
			EventHandler handler = this.ProblemInformationChanged;
			if (handler != null) handler(this, EventArgs.Empty);
		}

		private void RaiseSolveProblemCompleted(AsyncCompletedEventArgs e)
		{
			AsyncCompletedEventHandler handler = this.SolveProblemCompleted;
			if (handler != null) handler(this, e);
		}

		#endregion

		#region Operations with tracker

		private void DeleteProblemFromTracker()
		{
			TrackerProxy trackerProxy = CommonFunctions.OpenConnectionToTracker(
				Settings.Current.TrackerConnectionString, Settings.Current.UserName, Settings.Current.Password);
			bool wasError = false;

			try
			{
				trackerProxy.DeleteProblem(this.problem.Name);
				trackerProxy.Close();
			}
			catch (TimeoutException e)
			{
				Logger.Instance.AddRecord("Network operation timed out while deleting problem from tracker. " + CommonFunctions.UnwrapExceptionMessage(e));
				wasError = true;
			}
			catch (CommunicationException e)
			{
				Logger.Instance.AddRecord("There was a communication problem while deleting problem from tracker. " + CommonFunctions.UnwrapExceptionMessage(e));
				wasError = true;
			}

			if (wasError)
				trackerProxy.Abort();
		}

		private void NotifyTrackerAboutSolvedTask(int taskId, IEnumerable<string> userNames)
		{
			//// TODO: Make this sruff as robust as adding problem to the tracker (place it into the main loop)

			TrackerProxy trackerProxy = CommonFunctions.OpenConnectionToTracker(
				Settings.Current.TrackerConnectionString, Settings.Current.UserName, Settings.Current.Password);
			bool wasError = false;

			try
			{
				trackerProxy.NotifyAboutTaskCompletion(this.problem.Name, taskId, userNames);
				trackerProxy.Close();
			}
			catch (TimeoutException e)
			{
				Logger.Instance.AddRecord("Tracker network operation timed out. " + CommonFunctions.UnwrapExceptionMessage(e));
				wasError = true;
			}
			catch (CommunicationException e)
			{
				Logger.Instance.AddRecord("There was a communication problem with tracker. " + CommonFunctions.UnwrapExceptionMessage(e));
				wasError = true;
			}

			if (wasError)
				trackerProxy.Abort();
		}

		#endregion
	}
}